﻿
using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{

    public abstract class Index
    {
        public enum Recomendation
        {
            sell,
            buy,
            none
        };

        /**
         * data - dane z informacjami o cenach
         * day - dzień (sesja), dla której pobieramy wartośc wskaźnika - od 1 do (rozmiar data)
         */
        public abstract Recomendation eval(List<Record> data, int day);

    }

    public class Ema : Index
    {
        private int _days;

        public Ema(int days)
        {
            if (days < 1)
                throw new Exception("Liczba dni musi być liczbą dodatnią");

            _days = days;
        }

        public decimal evalAverage(List<Record> data, int day)
        {
            decimal average = data[day-1].Close;
            decimal sum = 1;
            decimal alpha = 2 / (_days + 1);
            decimal beta = (1-alpha);
            //Console.WriteLine((day - _days).ToString());

            for (int i = day - 2; i >= day - _days ; i --)
            {
                average += data[i].Close * beta;
                sum += beta;
                beta *= beta;
            }
            average /= sum;
            return average;
        }

        override public Recomendation eval(List<Record> data, int day)
        {
            int count = data.Count;

            if (day > count)
                throw new Exception("Próba obliczenia wskaźnika dla nieistniejącego dnia");

            if (day <= _days)
                return Recomendation.none;

            decimal average1 = evalAverage(data, day);
            decimal average2 = evalAverage(data, day - 1);

            if (average1 > data[day - 1].Close && average2 <= data[day - 2].Close)
                return Recomendation.buy;
            else if (average1 < data[day - 1].Close && average2 >= data[day - 2].Close)
                return Recomendation.sell;
            else
                return Recomendation.none;

        }
    }

    public class Wma : Index
    {
        private int _days;

        public Wma(int days)
        {
            if (days < 1)
                throw new Exception("Liczba dni musi być liczbą dodatnią");

            _days = days;
        }

        public decimal evalAverage(List<Record> data, int day)
        {
            decimal average = 0;
            decimal sum = 0;
            decimal n = _days;            

            for (int i = day - 1; i >= day - _days; i--)
            {
                average += n * data[i].Close;
                sum += n;
                n--;
            }
            average /= sum;
            return average;
        }

        override public Recomendation eval(List<Record> data, int day)
        {
            int count = data.Count;

            if (day > count)
                throw new Exception("Próba obliczenia wskaźnika dla nieistniejącego dnia");

            if (day <= _days)
                return Recomendation.none;

            decimal average1 = evalAverage(data, day);
            decimal average2 = evalAverage(data, day - 1);

            if (average1 > data[day - 1].Close && average2 <= data[day - 2].Close)
                return Recomendation.buy;
            else if (average1 < data[day - 1].Close && average2 >= data[day - 2].Close)
                return Recomendation.sell;
            else
                return Recomendation.none;

        }
    }

    public class Macd : Index
    {
        // days1 > days2, days1 > _daysSignal, days2 > _daysSignal
        private int _days1; 
        private int _days2;
        private int _daysSignal;

        private Ema ma1;
        private Ema ma2;

        public Macd(int days1, int days2, int daysSignal)
        {
            _days1 = days1;
            _days2 = days2;
            _daysSignal = daysSignal;
            ma1 = new Ema(days1);
            ma2 = new Ema(days2);
        }

        private decimal evalMacd(List<Record> data, int day)
        {
            decimal av1 = ma1.evalAverage(data, day);
            decimal av2 = ma2.evalAverage(data, day);

            return av1 - av2;
        }

        private decimal evalSignal(List<Record> data, int day)
        {
            decimal average = 0;
            for (int i = day - _daysSignal; i < day; i++)
            {
                average += evalMacd(data, i);
            }
            average /= new Decimal(_daysSignal);
            return average;
            
        }

        override public Recomendation eval(List<Record> data, int day)
        {
            int count = data.Count;

            if (day > count)
                throw new Exception("Próba obliczenia wskaźnika dla nieistniejącego dnia");

            int max = (_days1 > _days2) ? _days1 : _days2;
            if (day <= max + _daysSignal)
                return Recomendation.none;
           
            decimal macd = evalMacd(data, day);
            
            decimal signal = evalSignal(data, day);            

            decimal macdPrev = evalMacd(data, day - 1 );
            decimal signalPrev = evalSignal(data, day - 1);       
           
            decimal hPrev = signalPrev - macdPrev;
            decimal h = signal - macd;

            if (hPrev < 0 && h >= 0)
                return Recomendation.buy;
            else if (hPrev > 0 && h <= 0)
                return Recomendation.sell;
            else
                return Recomendation.none;
             
        }
    }

    public class MovingAverage : Index
    {
        private int _days;

        public MovingAverage(int days)
        {
            if (days < 1)
                throw new Exception("Liczba dni musi być liczbą dodatnią");


            _days = days;
        }

        public decimal evalAverage(List<Record> data, int day)
        {
            decimal average = 0;
            for (int i = day - _days; i < day; i++)
            {
                average += data[i].Close;
            }
            average /= new Decimal(_days);
            return average;
        }

        override public Recomendation eval(List<Record> data, int day)
        {
            int count = data.Count;

            if (day > count)
                throw new Exception("Próba obliczenia wskaźnika dla nieistniejącego dnia");

            if (day <= _days)
                return Recomendation.none;

            decimal average1 = evalAverage(data, day);
            decimal average2 = evalAverage(data, day - 1);

            if (average1 > data[day - 1].Close && average2 <= data[day - 2].Close)
                return Recomendation.buy;
            else if (average1 < data[day - 1].Close && average2 >= data[day - 2].Close)
                return Recomendation.sell;
            else
                return Recomendation.none;

        }
    }

    public class Rsi : Index
    {
        private int _days;

        public Rsi(int days)
        {
            if (days < 1)
                throw new Exception("Liczba dni musi być liczbą dodatnią");

            _days = days;
        }

        public Pair<decimal, decimal> evalAverages(List<Record> data, int day)
        {
            decimal averageA = 0;            
            decimal averageB = 0;
            int daysA = 0, daysB = 0;
            decimal diff;

            for (int i = day - _days; i < day; i++)
            {
                diff = data[i].Close - data[i-1].Close;
                if (diff > 0)
                {
                    averageA += diff;
                    daysA++;
                }
                else
                {
                    averageB += diff;
                    daysB++;
                }
            }
            if (daysA > 0)            
                averageA /= new Decimal(daysA);
            if (daysB > 0)            
                averageB /= new Decimal(_days);

            return new Pair<decimal, decimal>(averageA, averageB);
        }

        public decimal evalRsi(List<Record> data, int day)
        {
            Pair<decimal, decimal> average = evalAverages(data, day);

            
            decimal rs = (average.Second == 0) ? 100 : average.First / -average.Second;
            decimal rsi = 100 - (100 / (1 + rs));
            return rsi;
        }

        override public Recomendation eval(List<Record> data, int day)
        {
            int count = data.Count;

            if (day > count)
                throw new Exception("Próba obliczenia wskaźnika dla nieistniejącego dnia");

            if (day  <= _days + 1)
                return Recomendation.none;

            decimal rsi = evalRsi(data, day);
            decimal rsiPrev = evalRsi(data, day-1);

            if (rsi > 70 && rsiPrev <= 70)
                return Recomendation.sell;
            else if (rsi > 100 && rsiPrev <= 100)
                return Recomendation.sell;
            else if (rsi < 30 && rsiPrev >= 30)
                return Recomendation.buy;
            else if (rsi < 0 && rsiPrev >= 0)
                return Recomendation.buy;
            else
                return Recomendation.none;


        }
    }

    public class EmaMa : Index
    {

        // days1 > days2, days1 > _daysSignal, days2 > _daysSignal
        private int _days1; 
        private int _days2;       

        private Ema ema;
        private MovingAverage ma;

        public EmaMa(int days1, int days2)
        {
            _days1 = days1;
            _days2 = days2;
            ema = new Ema(days1);
            ma = new MovingAverage(days2);
        }

        override public Recomendation eval(List<Record> data, int day)
        {
            int count = data.Count;

            if (day > count)
                throw new Exception("Próba obliczenia wskaźnika dla nieistniejącego dnia");

            if (day <= _days1 + 1 || day <= _days2 + 1)
                return Recomendation.none;

            decimal average1 = ema.evalAverage(data, day);
            decimal average2 = ma.evalAverage(data, day);

            decimal average1Prev = ema.evalAverage(data, day - 1);
            decimal average2Prev = ma.evalAverage(data, day - 1);

            if (average2 > average1 && average2Prev <= average1Prev)
                return Recomendation.buy;
            else if (average2 < average1 && average2Prev >= average1Prev)
                return Recomendation.sell;
            else
                return Recomendation.none;
        }
    }

    public class WmaMa : Index
    {

        // days1 > days2, days1 > _daysSignal, days2 > _daysSignal
        private int _days1; 
        private int _days2;       

        private Wma wma;
        private MovingAverage ma;

        public WmaMa(int days1, int days2)
        {
            _days1 = days1;
            _days2 = days2;
            wma = new Wma(days1);
            ma = new MovingAverage(days2);
        }

        override public Recomendation eval(List<Record> data, int day)
        {
            int count = data.Count;

            if (day > count)
                throw new Exception("Próba obliczenia wskaźnika dla nieistniejącego dnia");

            if (day <= _days1 + 1 || day <= _days2 + 1)
                return Recomendation.none;

            decimal average1 = wma.evalAverage(data, day);
            decimal average2 = ma.evalAverage(data, day);

            decimal average1Prev = wma.evalAverage(data, day - 1);
            decimal average2Prev = ma.evalAverage(data, day - 1);

            if (average2 > average1 && average2Prev <= average1Prev)
                return Recomendation.buy;
            else if (average2 < average1 && average2Prev >= average1Prev)
                return Recomendation.sell;
            else
                return Recomendation.none;
        }
    }

    public class RWilliams : Index
    {     
        private int _days;        

        public RWilliams(int days)
        {
            _days = days;            
        }

        public decimal evalR(List<Record> data, int day)
        {
            decimal max = 0, min = -1;
            for (int i = day - _days; i < day; i++)
            {
                if (data[i].Close > max)
                {
                    max = data[i].Close;
                }

                if (data[i].Close < min || min == -1)
                {
                    min = data[i].Close;
                }
            }

            if (max - min == 0)
                max++;
            return ((data[day - 1].Close - max) / (max - min)) * 100;
        }

        override public Recomendation eval(List<Record> data, int day)
        {
            int count = data.Count;

            if (day > count)
                throw new Exception("Próba obliczenia wskaźnika dla nieistniejącego dnia");

            if (day <= _days + 1)
                return Recomendation.none;

            decimal r = evalR(data, day);
            decimal rPrev = evalR(data, day - 1);

            if (r < -80 && rPrev >= -80)            
                return Recomendation.buy;
            else if (r > -20 && rPrev <= -20)
                return Recomendation.sell;
            else
                return Recomendation.none;
        }
    }

    public class MoneyFlow : Index
    {

        private int _days;

        public MoneyFlow(int days)
        {
            _days = days;            
        }

        public Pair<decimal, decimal> getMoneyFlows(List<Record> data, int day)
        {
            decimal positive = 0;
            decimal negative = 0;

            decimal last = 0;
            bool first = true;
            for (int i = day - _days; i < day; i++)
            {
                decimal typish = (data[day - 1].High + data[day - 1].Low + data[day - 1].Close) / 3;
                decimal moneyFlow = typish * data[day - 1].Volume;

                if (first)
                {
                    first = false;
                    last = moneyFlow;
                }
                else
                {
                    if (last > moneyFlow)
                    {
                        negative += moneyFlow;                        
                    }
                    else
                    {
                        positive += moneyFlow;                        
                    }

                    last = moneyFlow;
                }
            }

            
            


            return new Pair<decimal, decimal>(positive, negative);
        }

        override public Recomendation eval(List<Record> data, int day)
        {
            int count = data.Count;

            if (day > count)
                throw new Exception("Próba obliczenia wskaźnika dla nieistniejącego dnia");

            if (day <= _days + 2)
                return Recomendation.none;            

            Pair<decimal, decimal> moneyFlows = getMoneyFlows(data, day);
            Pair<decimal, decimal> moneyFlowsPrev = getMoneyFlows(data, day-1);

            if (moneyFlows.Second == 0 || moneyFlowsPrev.Second == 0)
            {
                return Recomendation.none;
            }
            decimal mfi = 100 - 100 / (1 + moneyFlows.First / moneyFlows.Second);
            decimal mfiPrev = 100 - 100 / (1 + moneyFlowsPrev.First / moneyFlowsPrev.Second);

            if (mfi > 80 && mfiPrev <= 80)
                return Recomendation.sell;
            if (mfi < 20 && mfiPrev >= 20)
                return Recomendation.buy;
            else
                return Recomendation.none;
        }
    }
    
}
